package mk.jug.academy.service;

import mk.jug.academy.exception.BusinessException;
import mk.jug.academy.model.Course;
import mk.jug.academy.model.Page;
import mk.jug.academy.model.status.CourseStatus;
import mk.jug.academy.preconditions.ErrorCodes;
import mk.jug.academy.preconditions.PreconditionExceptions;
import mk.jug.academy.testing.bdd.IntegrationBehavior;
import mk.jug.academy.testing.factory.CourseTestFactory;
import mk.jug.academy.testing.util.TestHelper;

import org.springframework.beans.factory.annotation.Autowired;

import com.googlecode.instinct.marker.annotate.Specification;
import com.googlecode.instinct.marker.annotate.Specification.SpecificationState;
import com.googlecode.instinct.marker.annotate.Subject;

/**
 * An integration test unit regarding the lifecycle of a {@link Course} in the Java Academy.
 *
 * @author Ice.Penov
 */
public class CourseLifecycle extends IntegrationBehavior {
	
	@Autowired private CourseService courseService;
	
	@Subject private Course course;
	@Subject private Course course1;
	@Subject private Course course2;
	@Subject private Course course3;
	
	@Override
	protected void beforeHook() {
		TestHelper.init(courseService);
		course = CourseTestFactory.createCourse();
		course1 = CourseTestFactory.createCourse();
		course2 = CourseTestFactory.createCourse();
		course3 = CourseTestFactory.createCourse();
	}
	
	/**
	 * User action:
	 * An admin/lecturer tries to create a new course with incomplete info.
	 * 
	 * Expected behavior:
	 * 1. The process should fail with {@link IllegalArgumentException}
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToCreateNewCourseWhenMissingInformation() {
		//title
		course.setTitle(null);
		new ExceptionExpectation() {
			public void execute() {
				courseService.submitNewCourse(course);
			}
		}.checkFor(IllegalArgumentException.class, PreconditionExceptions.MISSING_TITLE);
	}
	
	/**
	 * User action:
	 * An admin/lecturer submits a new course in the system with the supplied information.
	 * 
	 * Expected behavior:
	 * 1. The course should be persistent in the database with the {@link CourseStatus#HATCHING} status.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void submitNewCourseWithCompleteInformation() {
		courseService.submitNewCourse(course);
		//should be available in all courses listing
		expect.that(courseService.listAll(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listAll(1, 2).getItems()).isOfSize(1);
		//should be available in hatching courses
		expect.that(courseService.listHatching(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listHatching(1, 2).getItems()).isOfSize(1);
		//but not in AVAILABLE or IN_PROGRESS courses
		expect.that(courseService.listAvailable(1, 2).getItems()).isEmpty();
		expect.that(courseService.listInProgress(1, 2).getItems()).isEmpty();		
	}
	
	/**
	 * User action:
	 * A {@link Course} in a state different than {@link CourseStatus#HATCHING} tries to hatch.
	 * 
	 * Expected behavior:
	 * Throw an {@link BusinessException} with an appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToHatchCourseInNotInTheHatchingState() {
		course.setStatus(CourseStatus.AVAILABLE);
		new ExceptionExpectation() {
			public void execute() {
				courseService.hatchCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_HATCHING_STATE);
		course.setStatus(CourseStatus.IN_PROGRESS);
		new ExceptionExpectation() {
			public void execute() {
				courseService.hatchCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_HATCHING_STATE);
		course.setStatus(CourseStatus.DEACTIVATED);
		new ExceptionExpectation() {
			public void execute() {
				courseService.hatchCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_HATCHING_STATE);
	}
	
	/**
	 * User action:
	 * A {@link Course} is hatched to the AVAILABLE state.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} should be present in the AVAILABLE pool.
	 * 2. The {@link Course} should no longer be present in the HATCHING pool.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void hatchCourseToAvailableState() {
		courseService.submitNewCourse(course);
		courseService.hatchCourse(course);
		expect.that(courseService.listHatching(1, 2).getItems()).isEmpty();
		expect.that(courseService.listAvailable(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listAvailable(1, 2).getItems()).isOfSize(1);
	}
	
	/**
	 * User action:
	 * A {@link Course} in a state different than {@link CourseStatus#AVAILABLE} is started.
	 *
	 * Expected behavior:
	 * Throw an {@link IllegalStateException} with an appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToStartCourseIfNotInTheAvailableState() {
		course.setStatus(CourseStatus.HATCHING);
		new ExceptionExpectation() {
			public void execute() {
				courseService.startCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);
		course.setStatus(CourseStatus.DEACTIVATED);
		new ExceptionExpectation() {
			public void execute() {
				courseService.startCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);
		course.setStatus(CourseStatus.IN_PROGRESS);
		new ExceptionExpectation() {
			public void execute() {
				courseService.startCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);		
	}
	
	/**
	 * User action:
	 * A {@link Course} is started.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} should be present in the IN_PROGRESS pool.
	 * 2. The {@link Course} should no longer be present in the AVAILABLE pool.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void startCourse() {
		courseService.submitNewCourse(course);
		course.setDescription("Description");
		courseService.hatchCourse(course);
		courseService.startCourse(course);
		expect.that(courseService.listAvailable(1, 2).getItems()).isEmpty();
		expect.that(courseService.listInProgress(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listInProgress(1, 2).getItems()).isOfSize(1);
	}
	
	/**
	 * User action:
	 * A {@link Course} that is not already running (in the {@link CourseStatus#IN_PROGRESS}) is finished.
	 * 
	 * Expected behavior:
	 * 1. Thrown an {@link BusinessException} with the appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToFinishCourseIfNotAlreadyRunning() {
		courseService.submitNewCourse(course);
		//try to finish a HATCHING course.
		new ExceptionExpectation() {
			public void execute() {
				courseService.finishCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_IN_PROGRESS_STATE);
		courseService.hatchCourse(course);
		//try to finish a AVAILABLE course.
		new ExceptionExpectation() {
			public void execute() {
				courseService.finishCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_IN_PROGRESS_STATE);
		courseService.deactivateCourse(course);
		//try to finish a deactivated course.
		new ExceptionExpectation() {
			public void execute() {
				courseService.finishCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_IN_PROGRESS_STATE);
	}
	
	/**
	 * User action:
	 * Finish a running {@link Course}.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} is no longer in the IN_PROGRESS pool.
	 * 2. The {@link Course} is now moved to the AVAILABLE pool.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void finishCourse() {
		courseService.submitNewCourse(course);
		courseService.hatchCourse(course);
		courseService.startCourse(course);
		courseService.finishCourse(course);
		//expectations:
		expect.that(courseService.listInProgress(1, 2).getItems()).isEmpty();
		expect.that(courseService.listAvailable(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listAvailable(1, 2).getItems()).isOfSize(1);
	}
	
	/**
	 * User action:
	 * A {@link Course} in state different than {@link CourseStatus#AVAILABLE} is deactivated.
	 * 
	 * Expected behavior:
	 * Throw an {@link IllegalStateException} with an appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToDeactivateCourseIfNotInActiveState() {
		course.setStatus(CourseStatus.HATCHING);
		new ExceptionExpectation() {
			public void execute() {
				courseService.deactivateCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);
		course.setStatus(CourseStatus.DEACTIVATED);
		new ExceptionExpectation() {
			public void execute() {
				courseService.deactivateCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);
		course.setStatus(CourseStatus.IN_PROGRESS);
		new ExceptionExpectation() {
			public void execute() {
				courseService.deactivateCourse(course);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_AVAILABLE_STATE);				
	}
	
	/**
	 * User action:
	 * A {@link Course} is deactivated.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} should be present in the DEACTIVATED pool.
	 * 2. The {@link Course} should no longer be present in the AVAILABLE pool.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void deactivateCourse() {
		courseService.submitNewCourse(course);
		courseService.hatchCourse(course);
		courseService.deactivateCourse(course);
		expect.that(courseService.listAvailable(1, 2).getItems()).isEmpty();
		expect.that(courseService.listDeactivated(1, 2).getItems()).containsItem(course);
		expect.that(courseService.listDeactivated(1, 2).getItems()).isOfSize(1);
	}
	
	/**
	 * User action:
	 * A {@link Course} is being edited.
	 * 
	 * Expected behavior:
	 * 1. All changes to be saved.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void editCourseProperties() {
		TestHelper.prepareAvailableCourse(course);
		course.setDescription("Changed");
		course.setTitle("Changed");
		courseService.saveChanges(course);
		Course loaded = courseService.load(course.getId());
		expect.that(loaded.getDescription()).isEqualTo("Changed");
		expect.that(loaded.getTitle()).isEqualTo("Changed");
	}
	
	/**
	 * User action:
	 * Fetch the {@link Course}s in a paged manner.
	 * 
	 * Expected behavior:
	 * List the records as expected.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void listCoursesUsingPaging() {
		TestHelper.prepareAvailableCourse(course1);
		TestHelper.prepareAvailableCourse(course2);
		TestHelper.prepareAvailableCourse(course3);
		Page<Course> page1 = courseService.listAvailable(1, 2);
		Page<Course> page2 = courseService.listAvailable(2, 2);
		Page<Course> page3 = courseService.listAvailable(3, 2);
		//expectations.
		//sizing.
		expect.that(page1.getTotalCount()).isEqualTo(3);
		expect.that(page2.getTotalCount()).isEqualTo(3);
		expect.that(page3.getTotalCount()).isEqualTo(3);
		expect.that(page1.getItems()).isOfSize(2);
		expect.that(page2.getItems()).isOfSize(1);
		expect.that(page3.getItems()).isEmpty();
		//check contents of pages.
		expect.that(page1.getItems()).containsItems(course3, course2);
		expect.that(page2.getItems()).containsItem(course1);
	}

}